# -*- coding: utf-8 -*-

"""
Mathematical functions and classes.


:Variables:
    PI_DIV_180 : float
        Just pi/180.0 used for converting  between degrees and radians
        (negavite because of the rotation direction)
    EPSILON : float
        the smallest value supported (initialized with sys.float_info.epsilon)
    TYPEPOINT : const
        use this cnonstant as the w coordinate for a vector to define a vector as point
    TYPEVECTOR : const
        use this cnonstant as the w coordinate for a vector to define a vector as vector

:Note:
    Make sure to know the difference between::

        u = Vec2()
        v = Vec2()
        w = Vec2()

        # only values are copied, same as u.x = w.x and u.y = w.y etc.
        u.copy_values(w)
        # reference, v is actually lost and points now to w
        v = w

    When u_sing ``v = w`` any change to w will also change v and vice versa
    because both point to the same `Vec2` instance.

"""


__all__ = ["Vec2", "Vec3", "sign", "Point2", "Point3",
            "TYPEPOINT", "TYPEVECTOR", "EPSILON", "PI_DIV_2", "PI_DIV_180"]


__author__ = "dr0iddr0id {at} gmail [dot] com (C) 2012"

import logging
_LOGGER = logging.getLogger('pyknic.mathematics')

import sys

#import math
from math import hypot as _hypot
from math import sin as _sin
from math import cos as _cos
from math import acos as _acos
from math import pi as PI
from math import atan2 as _atan2


EPSILON = sys.float_info.epsilon
PI_DIV_2 = PI / 2.0
PI_DIV_180 = PI / 180.0
TYPEPOINT = 1
TYPEVECTOR = 0


def sign(value):
    """
    Signum function. Computes the sign of a value.

    :Parameters:
        value : numerical
            Any number.

    :rtype: numerical

    :Returns:
        -1 if value was negative,
        1 if value was positive,
        0 if value was equal zero
    """
    assert isinstance(value, int) or isinstance(value, float)
    if 0 < value:
        return 1
    elif 0 > value:
        return -1
    else:
        return 0

# ------------------------------------------------------------------------------

class Vec2(object):
    """
    See `vectors`

    2D vector class.

    :Ivariables:
        x : int, float
            x value
        y : int, float
            y value

    """

    __slots__ = tuple('xyw')

    def __init__(self, x_coord, y_coord, w_coord=0):
        """
        Constructor.

        :Parameters:
            x_coord : int, float
                x value
            y_coord : int, float
                y value
            w_coord : int, float
                w value, the homogenous coordinate, 0 = vector, 1 = point
        """
        if __debug__:
            assert isinstance(x_coord, (int, complex, float))
            assert isinstance(y_coord, (int, complex, float))
            assert isinstance(w_coord, (int, complex, float))
        self.x = x_coord  # pylint: disable=C0103
        self.y = y_coord  # pylint: disable=C0103
        self.w = w_coord  # pylint: disable=C0103

    # -- properties -- #
    def copy_values(self, other):
        """
        Copy the values from other to own, e.g.:
        self.x = other.x
        self.y = other.y
        """
        assert isinstance(other, self.__class__)
        self.x = other.x
        self.y = other.y
        # self.w = other.w

    def set_from_iterable(self, other):
        """
        Sets the x and y values from other u_sing an iterator on it.
        """
        oiter = iter(other)
        try:
            self.x = next(oiter)
            self.y = next(oiter)
            self.w = next(oiter)
        except StopIteration:
            pass

    @property
    def length(self):
        """
        Calculates the length of the vector and returns it.
        """
        return _hypot(self.x, self.y)

    @length.setter
    def length(self, value):
        """Scale the vector to have a given length"""
        if self.length:
            val = (value / self.length)
            self.x *= val
            self.y *= val

    @property
    def length_sq(self):
        """
        Calculates the squared length of the vector and returns it.
        """
        return self.x * self.x + self.y * self.y

    @property
    def normal_left(self):
        """returns the right normal (perpendicular), not unit length"""
        return self.__class__(-self.y, self.x, self.w)

    @property
    def normal_right(self):
        """returns the left normal (perpendicular), not unit length"""
        return self.__class__(self.y, -self.x, self.w)

    @property
    def angle(self):
        """returns the angle in degrees"""
        return _atan2(self.y, self.x) / PI_DIV_180

    # -- repr methods -- #
    def __str__(self):
        return "<%s(%s, %s, w=%s) at %s>" % (self.__class__.__name__,
                                        self.x,
                                        self.y,
                                        self.w,
                                        hex(id(self)))

#    def __str__(self):
#        return u"<%s(%s, %s)>" %(self.__class__.__name__, self.x, self.y)
    __repr__ = __str__

    # -- math -- #
    def __add__(self, other):
        """+ operation"""
        assert isinstance(other, self.__class__)
        return self.__class__(self.x + other.x, self.y + other.y, self.w + other.w)

    def __iadd__(self, other):
        """+= operation"""
        assert isinstance(other, self.__class__)
        self.x += other.x
        self.y += other.y
        self.w += other.w
        return self

    def __sub__(self, other):
        """- operation"""
        assert isinstance(other, self.__class__)
        return self.__class__(self.x - other.x, self.y - other.y, self.w - other.w)

    def __isub__(self, other):
        """-= operation"""
        assert isinstance(other, self.__class__)
        self.x -= other.x
        self.y -= other.y
        self.w -= other.w
        return self

    def __mul__(self, scalar):
        """* operator, only scalar multiplication, for other use the methods"""
        return self.__class__(self.x * scalar,  self.y * scalar, self.w)
    __rmul__ = __mul__

    def __imul__(self, scalar):
        """ \*= multiplication, scalar only"""
        self.x *= scalar
        self.y *= scalar
        self.w *= scalar
        return self

    def __len__(self):
        """returns always 2"""
        return 2

    def __div__(self, scalar):
        """/ operator, only scalar"""
        return self.__class__(self.x / scalar, self.y / scalar)

    def __idiv__(self, scalar):
        """/= operator, only scalar"""
        self.x /= scalar
        self.y /= scalar
        self.w /= scalar
        return self

    def __neg__(self):
        """- operator, same as -1 * v"""
        return self.__class__(-self.x, -self.y, -self.w)

    def __pos__(self):
        """+ operator"""
        return self.__class__(abs(self.x), abs(self.y), abs(self.w))

    # -- comparison -- #
    def __eq__(self, other):
        """ == operator, vectors are equal when components are equal, might not
        make much sense _since u_sing a tolerance would be better"""
        if isinstance(other, self.__class__):
            return self.get_distance(other) <= EPSILON
            # return self.x == other.x and self.y == other.y
        return False

    def __ne__(self, other):
        """same as 'not __eq__'"""
        return not self.__eq__(other)

    def __hash__(self):
        return id(self)

    # -- items access -- #
    def __getitem__(self, key):
        """[] operator, slow"""
        return (self.x, self.y, self.w)[key]

    def __iter__(self):
        """iterator, slow"""
        return iter((self.x, self.y, self.w))

    # -- additional methods -- #
    # def as_tuple(self, include_w=False):
    def as_tuple(self, round_func=None, *args):
        """
        Returns a tuple containing the vector values.

        :Parameters:
            round_func : func
                defaults to None, if set, its a rounding function to apply like round, int, etc.
            args :
                the arguments for the found_func if set

        :rtype: tuple
        :Returns: (x, y)

        """
        # if include_w:
            # return self.x, self.y, self.w
        # return self.x, self.y
        if round_func:
            return (round_func(self.x, *args), round_func(self.y, *args))
        return (self.x, self.y)

    def round(self, round_func=round, *args):
        """
        Round values according to the given round_func.

        Example::

            v = Vector(1.75, 3.33)
            w = v.round() # w == Vector(2, 3)
            x = v.round(round, 1) # x == Vector(1.8, 3.3)
            y = v.round(int) # y == Vector(1, 3)

        :Parameters:
            round_func[round] : function
                the rounding function to use, defaults to round
            args : args
                the arguments to use for the rounding function.
        """
        self.x = round_func(self.x, *args)
        self.y = round_func(self.y, *args)

    def rounded(self, round_func=round, *args):
        """
        Returns a new vector with rounded components u_sing the rounding function.

        Example::

            v = Vector(1.75, 3.33)
            w = v.rounded() # w == Vector(2, 3)
            x = v.rounded(round, 1) # x == Vector(1.8, 3.3)
            y = v.rounded(int) # y == Vector(1, 3)

        :Parameters:
            round_func[round] : function
                the rounding function to use, defaults to round
            args : args
                the arguments to use for the rounding function.
        """
        return self.__class__(round_func(self.x, *args), round_func(self.y, *args), w_coord=self.w)

    def normalize(self):
        """
        Make the vector unit length.

        :Returns: the length it has before normalizing.
        """
        leng = self.length
        if leng > 0:
            self.x /= leng
            self.y /= leng
        return leng

    @property
    def normalized(self):
        """
        Returns a new vector with unit length.
        """
        leng = self.length
        if leng > 0:
            return self.__class__(self.x / leng, self.y / leng)
        else:
            return self.__class__(0, 0)

    # def clone(self):
        # """
        # Clone the vector.

        # :rtype: `Vec2`
        # :Returns: a copy of itself.
        # """
        # return self.__class__(self.x, self.y)

    def dot(self, other):
        """
        Dot product.
        """
        assert isinstance(other, self.__class__)
        return self.x * other.x + self.y * other.y

    def cross(self, other):
        """
        Cross product.

        :Parameters:
            other : `Vec2`
                The second vector for the cross product.

        :rtype: float
        :Returns: z value of the cross product (_since x and y would be 0).
        :Note: a.cross(b) == - b.cross(a)
        """
        assert isinstance(other, self.__class__)
        return self.x * other.y - self.y * other.x

    def project_onto(self, other):
        """
        Project this vector onto another one.

        :Parameters:
            other : `Vec2`
                The other vector to project onto.

        :rtype: `Vec2`
        :Returns: The projected vector.
        """
        assert isinstance(other, self.__class__)
        return self.dot(other) / other.length_sq * other

    def reflect(self, normal):
        """normal should be normalized unitlength"""
        assert isinstance(normal, self.__class__)
        return self - 2 * self.dot(normal) * normal

    def reflect_tangent(self, tangent):
        """tangent should be normalized, unitlength"""
        assert isinstance(tangent, self.__class__)
        return 2 * tangent.dot(self) * tangent - self

    def face_forward(self, normal_reference):
        """
        Returns a vector faceing forward to the normal_reference vector.

        :Parameters:
            normal_reference : Vector
                the reference vector

        :Returns: n if self.dot(normal_reference) < 0, -n otherwise
        """
        if self.dot(normal_reference) < 0:
            self.x *= -1
            self.y *= -1

    def refract(self, normal, r_index):
        """
        computes the direction of a refracted ray if i specifies the normalized(!)
        direction of the incoming ray and n specifies the normalized(!) normal vector
        of the interface of two optical media (e.g. air and water). The vector n
        should point to the side from where i is coming, i.e. the dot product of n
        and i should be negative. The floating-point number r is the ratio of the
        refractive index of the medium from where the ray comes to the refractive
        index of the medium on the other side of the surface. Thus, if a ray comes
        from air (refractive index about 1.0) and hits the surface of water
        (refractive index 1.33), then the ratio r is 1.0 / 1.33 = 0.75.
        The computation of the function is:

        float d = 1.0 - r * r * (1.0 - dot(n, i) * dot(n, i));
        if (d < 0.0) return TYPE(0.0); // total internal reflection
        return r * i - (r * dot(n, i) + sqrt(d)) * n;

        As the code shows, the function returns a vector of length 0 in the case
        of total internal reflection (see the entry in Wikipedia), i.e. if the ray
        does not pass the interface between the two materials.

        :Parameters:
            normal : Vector
                the normal vector of the interface pointing in direction of self
            r_index : float
                ratio of the refractive index of both materials.

        :returns:
            a zero vector if total internal reflection, otherwise the refracted vector

        """
        _cos_normal = self.dot(normal)
        assert _cos_normal < 0, "normal not pointing to the side of incident vector"
        assert normal.length == 1.0, "normal is not unit length"
        assert self.length == 1.0, "incident vector is not unit length"
        dval = 1.0 - r_index * r_index * (1.0 - _cos_normal * _cos_normal)
        if dval <= 0.0:
            return Vec2(0.0, 0.0) # total internal reflection
        return r_index * self - (r_index * _cos_normal + dval**0.5) * normal

    def rotate(self, deg):
        """Rotates the vector about the angle (deg), + clockwise, - ccw"""
        rad = deg * PI_DIV_180
        _sin_val = _sin(rad)
        _cos_val = _cos(rad)
        xcoord = self.x
        self.x = _cos_val * xcoord - _sin_val * self.y
        self.y = _sin_val * xcoord + _cos_val * self.y

    def rotated(self, deg):
        """
        Returns a rotated the vector.
        Returns a new vector, rotated about angle (deg), + clockwise, - ccw

        :Parameters:
            deg : int, float
                deg to rotate, deg > 0 : anti-clockwise rotation
        """
        if deg == 0:
            return self.__class__(self.x, self.y, self.w)
        elif deg == -90:
            return self.__class__(self.y, -self.x, self.w)
        elif deg == 90:
            return self.__class__(-self.y, self.x, self.w)
        elif deg == 180 or deg == -180:
            return self.__class__(-self.x, -self.y, self.w)
        else:
            rad = deg * PI_DIV_180
            cval = _cos(rad)
            sval = _sin(rad)
            return self.__class__(self.x * cval - self.y * sval,
                                  self.x * sval + self.y * cval, self.w)

    def scaled(self, scale):
        """
        Returns a new vector scaled to given length.

        :Returns: `Vec2` with length scale
        """
        if self.length:
            scale = scale / self.length
            return self.__class__(scale * self.x, scale * self.y)
        return self.__class__(0.0, 0.0)

    def rotate_to(self, angle_degrees):
        """rotates the vector to the given angle (degrees)."""
        self.rotate(angle_degrees - self.angle)

    def get_angle_between(self, other):
        """Returns the angle between the vectors."""
        assert isinstance(other, self.__class__)
        length = self.length
        olen = other.length
        if length and olen:
            return  _acos(self.dot(other) / (length * olen)) / PI_DIV_180
        return 0

    def get_full_angle_between(self, other):
        """
        Get angle between this and other vector.

        :Parameters:
            other : `Vec3`
                other vector

        :rtype: float

        :Returns:
            Angle in degrees.
        """
        assert isinstance(other, self.__class__)
        length = self.length
        olen = other.length
        if length and olen:
            return (_atan2(other.y, other.x) - _atan2(self.y, self.x)) / PI_DIV_180
        return 0

    def get_distance_sq(self, other):
        """
        Distance squared this and other point
        (represented as vector from origin).

        :Parameters:
            other : `Vec2`
                The second vector for the distance.
        :rtype: float
        """
        assert isinstance(other, self.__class__)
        # delta_x = self.x - other.x
        # delta_y = self.y - other.y
        # return delta_x * delta_x + delta_y * delta_y
        return (self.x - other.x) ** 2 + (self.y - other.y) ** 2

    def get_distance(self, other):
        """
        Distance this and other point (represented as vector from origin).

        :Parameters:
            other : `Vec2`
                The second vector for the distance.
        :rtype: float
        """
        assert isinstance(other, self.__class__)
        return _hypot(self.x - other.x, self.y - other.y)

    @staticmethod
    def ZERO():
        """returns the zero vector: Vec(0.0, 0.0)"""
        return Vec2(0.0, 0.0)

    @staticmethod
    def UNIT_X():
        """returns the x-axis unit vector: Vec(1.0, 0.0)"""
        return Vec2(1.0, 0.0)

    @staticmethod
    def UNIT_Y():
        """returns the y-axis unit vector: Vec(0.0, 1.0)"""
        return Vec2(0.0, 1.0)


# ------------------------------------------------------------------------------

def Point2(x=0.0, y=0.0):
    """
    Returns a vector instance configured as TYPEPOINT.

    :Parameters:
        x[0.0] :
            x component of vector
        y[0.0] :
            y component of vector
    """
    return Vec2(x, y, TYPEPOINT)


# ------------------------------------------------------------------------------

class Vec3(object):
    """
    See `vectors`

    3D Vector.


    :Ivariables:
        x : int, float
            x, value
        y : int, float
            y, value
        z : int, float
            z, value
    """

    __slots__ = tuple('xyzw')

    def __init__(self, x_coord, y_coord, z_coord=0, w_coord=0):
        """
        Constructor.

        :Parameters:
            x_coord : int, float
                x value
            y_coord : int, float
                y value
            z_coord : int, float
                z value, defaults to 0
            w_coord : int, float
                w, homogenous coordinate, defaults to 0 for a vector, 1 for a point
        """
        if __debug__:
            assert isinstance(x_coord,
                                (int, complex, float, long))
            assert isinstance(y_coord,
                                (int, complex, float, long))
            assert isinstance(z_coord,
                                (int, complex, float, long))
            assert isinstance(w_coord,
                                (int, complex, float, long))
        self.x = x_coord # pylint: disable=C0103
        self.y = y_coord # pylint: disable=C0103
        self.z = z_coord # pylint: disable=C0103
        self.w = w_coord # pylint: disable=C0103

    # -- properties -- #
    def copy_values(self, other):
        """Copy the values from other, e.g.:
            self.x = other.x
            self.y = other.y
            self.z = other.z
        """
        assert isinstance(other, self.__class__)
        self.x = other.x
        self.y = other.y
        self.z = other.z
        # self.w = other.w

    def set_from_iterable(self, other):
        """Set x,y and z u_sing an iterator on other"""
        oiter = iter(other)
        try:
            self.x = next(oiter)
            self.y = next(oiter)
            self.z = next(oiter)
            self.w = next(oiter)
        except StopIteration:
            pass

    @property
    def length(self):
        """
        Returns the length of the vector
        get/set the length of vector
        returns length and change the length if set::

        v = Vec3(x, y, z)
        length = v.length
        v.length = 10 # vectors is scaled so length is 10 now

        """
        # x ** 5 is faster than sqrt(x)
        return (self.x * self.x + self.y * self.y + self.z * self.z) ** 0.5

    @length.setter
    def length(self, value):
        """Scale the vector to have a given length"""
        if self.length:
            val = (value / self.length)
            self.x *= val
            self.y *= val
            self.z *= val

    @property
    def length_sq(self):
        """Returns the squared length"""
        return self.x * self.x + self.y * self.y + self.z * self.z

    @property
    def normal_left(self):
        """
        not unit length
        returns left normal, not unit length, z is just copied
        """
        return self.__class__(-self.y, self.x, self.z, self.w)

    @property
    def normal_right(self):
        """
        not unit length
        returns the right normal, not unitlength, z is just copied
        """
        return self.__class__(self.y, -self.x, self.z, self.w)

    @property
    def angle(self):
        """
        returns the angle for the x and y component
        read only, returns angle of vector, 0 is at 3 o'clock
        """
        return _atan2(self.y, self.x) / PI_DIV_180

    # -- repr methods -- #
    def __str__(self):
        return "<%s(%s, %s, %s, w=%s) at %s>" % (self.__class__.__name__,
                                            self.x,
                                            self.y,
                                            self.z,
                                            self.w,
                                            hex(id(self)))
    __repr__ = __str__

    # -- math -- #


    def __add__(self, other):
        """+ opertaor"""
        assert isinstance(other, self.__class__)
        return self.__class__(self.x + other.x,
                              self.y + other.y,
                              self.z + other.z,
                              self.w + other.w)

    def __iadd__(self, other):
        """+= opertaor"""
        assert isinstance(other, self.__class__)
        self.x += other.x
        self.y += other.y
        self.z += other.z
        self.w += other.w
        return self

    def __sub__(self, other):
        """- opertaor"""
        assert isinstance(other, self.__class__)
        return self.__class__(self.x - other.x,
                              self.y - other.y,
                              self.z - other.z,
                              self.w - other.w)

    def __isub__(self, other):
        """-= opertaor"""
        assert isinstance(other, self.__class__)
        self.x -= other.x
        self.y -= other.y
        self.z -= other.z
        self.w -= other.w
        return self

    def __mul__(self, scalar):
        """\* opertaor with scalar only, for `dot` or `cross` see
        corresponding methods
        """
        return self.__class__(self.x * scalar, self.y * scalar, self.z * scalar, self.w)
    __rmul__ = __mul__

    def __imul__(self, scalar):
        """\*= opertaor with a scalar"""
        self.x *= scalar
        self.y *= scalar
        self.z *= scalar
        self.w *= scalar
        return self

    def __len__(self):
        """returns always 3"""
        return 3

    def __div__(self, scalar):
        """/ opertaor, scalar only"""
        return self.__class__(self.x / scalar, self.y / scalar, self.z / scalar)

    def __idiv__(self, scalar):
        """/= opertaor, scalar only"""
        self.x /= scalar
        self.y /= scalar
        self.z /= scalar
        self.w /= scalar
        return self

    def __neg__(self):
        """- opertaor, same as -1 \* vec"""
        return self.__class__(-self.x, -self.y, -self.z, -self.w)

    def __pos__(self):
        return self.__class__(abs(self.x), abs(self.y), abs(self.z), abs(self.w))

    # -- comparison -- #
    def __eq__(self, other):
        """ == operator, vectors are equal when components are equal, might
        not make much sense _since u_sing a tolerance would be better"""
        if isinstance(other, self.__class__):
            return self.get_distance(other) <= EPSILON
            # return self.x == other.x and self.y == other.y and self.z == other.z
        return False

    def __ne__(self, other):
        """same as not __eq__"""
        return not self.__eq__(other)

    def __hash__(self):
        return id(self)

    # -- items access -- #
    def __getitem__(self, key):
        """[] operator, slow"""
        return (self.x, self.y, self.z, self.w)[key]

    def __iter__(self):
        """returns an iterator"""
        return iter((self.x, self.y, self.z, self.w))

    # -- additional methods -- #
    def as_tuple(self, round_func=None, *args):
        """
        Returns a tuple containing the vector values.

        :Parameters:
            round_func : func
                defaults to None, if set, its a rounding function to apply like round, int, etc.
            args :
                the arguments for the found_func if set

        :rtype: tuple
        :Returns: (x, y, z)
        """
        # return self.x, self.y, self.z
        if round_func:
            return (round_func(self.x, *args), round_func(self.y, *args), round_func(self.z, *args))
        return (self.x, self.y, self.z)

    def as_xy_tuple(self, round_func=None, *args):
        """
        returns tuple (x, y), z is supressed
        """
        if round_func:
            return (round_func(self.x, *args), round_func(self.y, *args))
        return self.x, self.y

    def round(self, round_func=round, *args):
        """
        Rounds components u_sing the rounding function.

        Example::

            v = Vector(1.75, 3.33, 4.44)
            w = v.rounded() # w == Vector(2, 3, 4)
            x = v.rounded(round, 1) # x == Vector(1.8, 3.3, 4.4)
            y = v.rounded(int) # y == Vector(1, 3, 4)

        :Parameters:
            round_func[round] : function
                the rounding function to use, defaults to round
            args : args
                the arguments to use for the rounding function.
        """
        self.x = round_func(self.x, *args)
        self.y = round_func(self.y, *args)
        self.z = round_func(self.z, *args)

    def rounded(self, round_func=round, *args):
        """
        Returns a new vector with rounded components u_sing the rounding function.

        Example::

            v = Vector(1.75, 3.33, 4.44)
            w = v.rounded() # w == Vector(2, 3, 4)
            x = v.rounded(round, 1) # x == Vector(1.8, 3.3, 4.4)
            y = v.rounded(int) # y == Vector(1, 3, 4)

        :Parameters:
            round_func[round] : function
                the rounding function to use, defaults to round
            args : args
                the arguments to use for the rounding function.
        """
        return self.__class__(round_func(self.x, *args),
                              round_func(self.y, *args),
                              round_func(self.z, *args))

    def normalize(self):
        """
        Make the vector unit length. If length is 0 then nothing is done.

        :Returns: the length it has before normalizing.
        """
        leng = self.length
        if leng > 0:
            self.x /= leng
            self.y /= leng
            self.z /= leng
        return leng

    @property
    def normalized(self):
        """Returns a new, normalized vector"""
        leng = self.length
        if leng > 0:
            return self.__class__(self.x / leng, self.y / leng,  self.z / leng)
        else:
            return self.__class__(0, 0, 0)

    # def clone(self):
        # """
        # Clone the vector.

        # :rtype: `Vec3`
        # :Returns: a copy of itself.
        # """
        # return self.__class__(self.x, self.y, self.z)

    def dot(self, other):
        """
        Dot product.

        :Parameters:
            other : `Vec3`
                Other vector.

        :rtype: float

        :Returns:
            self.dot(other)
        """
        assert isinstance(other, self.__class__)
        return self.x * other.x + self.y * other.y + self.z * other.z

    def cross(self, other):
        """
        Cross product.

        :Parameters:
            other : `Vec3`
                The second vector for the cross product.

        :rtype: `Vec3`
        :Returns: vector resulting from the cross product.
        :Note: a.cross(b) == - b.cross(a)
        """
        assert isinstance(other, self.__class__)
        return self.__class__(self.y * other.z - self.z * other.y,
                              self.z * other.x - self.x * other.z,
                              self.x * other.y - self.y * other.x)

    def project_onto(self, other):
        """
        Project this vector onto another one.

        :Parameters:
            other : `Vec3`
                The other vector to project onto.

        :rtype: `Vec3`
        :Returns: The projected vector.
        """
        assert isinstance(other, self.__class__)
        return self.dot(other) / other.length_sq * other

    def reflect(self, normal):
        """
        Reflects this vector at a normal.

        :Parameters:
            normal : `Vec3`
                normal should be normalized unitlength

        :rtype: `Vec3`

        :Returns:
            Reflected vector.

        """
        assert isinstance(normal, self.__class__)
        return self - 2 * self.dot(normal) * normal

    def reflect_tangent(self, tangent):
        """
        Reflects vector at a tangent.

        :Parameters:
            tangent : `Vec3`
                tangent should be normalized, unitlength

        :rtype: `Vec3`

        :Returns:
            Reflected vector.
        """
        assert isinstance(tangent, self.__class__)
        return 2 * tangent.dot(self) * tangent - self

    def face_forward(self, normal_reference):
        """
        Returns a vector faceing forward to the normal_reference vector.

        :Parameters:
            normal_reference : Vector
                the reference vector

        :Returns: n if self.dot(normal_reference) < 0, -n otherwise
        """
        if self.dot(normal_reference) < 0:
            self.x *= -1
            self.y *= -1
            self.z *= -1

    def refract(self, normal, r_index):
        """
        computes the direction of a refracted ray if i specifies the normalized(!)
        direction of the incoming ray and n specifies the normalized(!) normal vector
        of the interface of two optical media (e.g. air and water). The vector n
        should point to the side from where i is coming, i.e. the dot product of n
        and i should be negative. The floating-point number r is the ratio of the
        refractive index of the medium from where the ray comes to the refractive
        index of the medium on the other side of the surface. Thus, if a ray comes
        from air (refractive index about 1.0) and hits the surface of water
        (refractive index 1.33), then the ratio r is 1.0 / 1.33 = 0.75.
        The computation of the function is:

        float d = 1.0 - r * r * (1.0 - dot(n, i) * dot(n, i));
        if (d < 0.0) return TYPE(0.0); // total internal reflection
        return r * i - (r * dot(n, i) + sqrt(d)) * n;

        As the code shows, the function returns a vector of length 0 in the case
        of total internal reflection (see the entry in Wikipedia), i.e. if the ray
        does not pass the interface between the two materials.

        :Parameters:
            normal : Vector
                the normal vector of the interface pointing in direction of self
            r_index : float
                ratio of the refractive index of both materials.

        :returns:
            a zero vector if total internal reflection, otherwise the refracted vector

        """
        _cos_normal = self.dot(normal)
        assert _cos_normal < 0, "normal not pointing to the side of incident vector"
        assert normal.length == 1.0, "normal is not unit length"
        assert self.length == 1.0, "incident vector is not unit length"
        dval = 1.0 - r_index * r_index * (1.0 - _cos_normal * _cos_normal)
        if dval <= 0.0:
            return Vec2(0.0, 0.0) # total internal reflection
        return r_index * self - (r_index * _cos_normal + dval**0.5) * normal

    def rotated(self, deg, axis_vec3): # pylint: disable=R0914
        """
        Rotates the vector around given axis and returns a new vector.

        :Parameters:
            deg : float
                angle in deg
            axis_vec3 : `Vec3`
                axis to rotate around

        :rtype: `Vec3`

        :Returns: Rotated vector.

        :Note: see: http://www.cprogramming.com/tutorial/3d/rotation.html

        """
        assert isinstance(axis_vec3, self.__class__)
        #// http://www.cprogramming.com/tutorial/3d/rotation.html
        #
        #//tXX + c  tXY + sZ  tXZ - sY  0
        #//tXY-sZ   tYY + c   tYZ + sX  0
        #//tXY + sY tYZ - sX  tZZ + c   0
        #//0        0         0         1
        #
        #//Where c = _cos (theta), s = _sin (theta), t = 1-_cos (theta),
        #  and <X,Y,Z> is the unit vector representing the arbitary axis

        theta = -deg * PI_DIV_180
        co_ = _cos(theta)
        si_ = _sin(theta)
        th_ = 1 - co_
        xcoord, ycoord, zcoord = axis_vec3.normalized.as_tuple()
        xy_ = xcoord * ycoord
        yz_ = ycoord * zcoord
        sx_ = si_ * xcoord
        sy_ = si_ * ycoord
        sz_ = si_ * zcoord

        _x_ = self.x
        _y_ = self.y
        _z_ = self.z

        return self.__class__((th_ * xcoord * xcoord + co_) * _x_ + (th_ * xy_    + sz_)          * _y_ + (th_ * xcoord  * zcoord - sy_) * _z_, # pylint: disable=C0301
                              (th_ * xy_    - sz_)          * _x_ + (th_ * ycoord * ycoord + co_) * _y_ + (th_ * yz_     + sx_)          * _z_, # pylint: disable=C0301
                              (th_ * xy_    + sy_)          * _x_ + (th_ * yz_    - sx_)          * _y_ + (th_ * zcoord  * zcoord + co_) * _z_) # pylint: disable=C0301

    def scaled(self, scale):
        """
        Returns a vector scaled to given length.

        :Returns: `Vec3` with lange scale
        """
        if self.length:
            scale = scale / self.length
            return self.__class__(scale * self.x, scale * self.y, scale * self.z)
        return self.__class__(0.0, 0.0)

    # def rotate_to(self, angle_degrees, axis):
        # """
        # Would rotate to a given angle, but since there is not 0 angle direction for an 
        # arbitrary axis this method is not implemented.
        # """
        # raise NotImplementedError("this is not possible for an arbitrary axis")

    def get_angle_between(self, other):
        """
        Get angle between this and other vector.

        :Parameters:
            other : `Vec3`
                other vector

        :rtype: float

        :Returns:
            Angle in degrees.
        """
        assert isinstance(other, self.__class__)
        length = self.length
        olen = other.length
        if length and olen:
            return  _acos(self.dot(other) / (length * olen)) / PI_DIV_180
        return 0

    def get_full_angle_between(self, other):
        """
        Get angle between this and other vector.

        :Parameters:
            other : `Vec3`
                other vector

        :rtype: float

        :Returns:
            Angle in degrees.
        """
        assert isinstance(other, self.__class__)
        length = self.length
        olen = other.length
        if length and olen:
            return \
                  (_atan2(other.y, other.x) - _atan2(self.y, self.x)) / PI_DIV_180
        return 0

    def get_distance_sq(self, other):
        """
        Distance squared from this and other point (represented as vector
        from origin).

        :Parameters:
            other : `Vec2`
                The second vector for the distance.
        :rtype: float
        """
        assert isinstance(other, self.__class__)
        # delta_x = self.x - other.x
        # delta_y = self.y - other.y
        # delta_z = self.z - other.z
        # return delta_x * delta_x + delta_y * delta_y + delta_z * delta_z
        return (self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2

    def get_distance(self, other):
        """
        Distance this and other point (represented as vector from origin).

        :Parameters:
            other : `Vec2`
                The second vector for the distance.
        :rtype: float
        """
        assert isinstance(other, self.__class__)
        # delta_x = self.x - other.x
        # delta_y = self.y - other.y
        # delta_z = self.z - other.z
        # x ** 0.5 is faster than sqrt(x)
        # return (delta_x * delta_x + delta_y * delta_y + delta_z * delta_z) ** 0.5
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2) ** 0.5

    @staticmethod
    def ZERO():
        """returns the zero vector: Vec(0.0, 0.0, 0.0)"""
        return Vec3(0.0, 0.0, 0.0)

    @staticmethod
    def UNIT_X():
        """returns the x-axis unit vector: Vec(1.0, 0.0, 0.0)"""
        return Vec3(1.0, 0.0, 0.0)

    @staticmethod
    def UNIT_Y():
        """returns the y-axis unit vector: Vec(0.0, 1.0, 0.0)"""
        return Vec3(0.0, 1.0, 0.0)

    @staticmethod
    def UNIT_Z():
        """returns the z-axis unit vector: Vec(0.0, 0.0, 1.0)"""
        return Vec3(0.0, 0.0, 1.0)

# ------------------------------------------------------------------------------

def Point3(x=0.0, y=0.0, z=0.0):
    """
    Returns a vector instance configured as TYPEPOINT.

    :Parameters:
        x[0.0] :
            x component of vector
        y[0.0] :
            y component of vector
        z[0.0] :
            z component of vector
    """
    return Vec3(x, y, z, TYPEPOINT)

# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------

# -------------------------------------------------------------------------------
